package wheel.servlet.http;

import wheel.conversion.Converter;
import wheel.conversion.ConverterService;

import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import java.lang.reflect.Method;
import java.lang.reflect.Array;

import java.util.Date;
import java.util.SortedSet;
import java.util.TreeSet;
import java.util.Comparator;
import java.util.Map;
import java.util.HashMap;
import java.util.Enumeration;

import com.thoughtworks.paranamer.Paranamer;
import com.thoughtworks.paranamer.BytecodeReadingParanamer;

import java.io.IOException;
import java.io.PrintWriter;

public abstract class DryServlet extends HttpServlet {
    protected HttpServletRequest request;
    protected HttpServletResponse response;
    
    public abstract String getURL();
    
    protected void doPost(HttpServletRequest request, HttpServletResponse response) {
        processRequest(request, response);
    }
    
    protected void doGet(HttpServletRequest request, HttpServletResponse response) {
        processRequest(request, response);
    }
    
    private void processRequest(HttpServletRequest request, HttpServletResponse response) {
        this.request = request;
        this.response = response;
        
        try {
            String methodName = getMethodName(request);
            callMethod(request, methodName);
        } catch (Exception e) {
            e.printStackTrace();
            // TODO: implement forwarding to error page
        }
    }
    
    private String getMethodName(HttpServletRequest request) {
        String method = request.getRequestURI().substring(request.getContextPath().length());
        if (method != null && !"".equals(method) && !"/".equals(method)) {
            return method.substring(method.lastIndexOf("/") + 1, method.length());
        } else {
            return null;
        }
    }
    
    private void callMethod(HttpServletRequest request, String methodName) throws Exception {
        if (methodName == null) throw new Exception("Method name missing");
        
        SortedSet<Method> methods = new TreeSet<Method>(getMethodComparator());
        for (Method method : this.getClass().getMethods()) {
            if (method.getName().equalsIgnoreCase(methodName)) {
                methods.add(method);
            }
        }
        
        Method targetMethod = null;
        String[] paramNames = null;
        Class[] paramTypes = null;
        try {
            Paranamer paranamer = new BytecodeReadingParanamer();
            for (Method method : methods) {
                paramNames = paranamer.lookupParameterNames(method);
                if (isTargetMethod(request, paramNames)) {
                    targetMethod = method;
                    paramTypes = method.getParameterTypes();
                    break;
                }
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
        
        if (targetMethod == null) 
                throw new Exception("Method with this name and parameters is missing");
        
        targetMethod.invoke(this, getMethodParams(request, paramNames, paramTypes));
    }
    
    private boolean isTargetMethod(HttpServletRequest request, String[] paramNames) {
        boolean result = true;
        if (paramNames != null && 
                paramNames.length > 0) {
            Map<String, String> requestParams = request.getParameterMap();
            for (String paramName : paramNames) {
                if (!requestParams.containsKey(paramName)) {
                    result = false;
                    break;
                }
            }
        }
        return result;
    }
    
    private Object[] getMethodParams(HttpServletRequest request, 
            String[] paramNames, Class[] paramTypes) {
        Object[] params = null;
        if (paramNames != null && 
                paramTypes != null && 
                paramNames.length == paramTypes.length) {
            params = new Object[paramNames.length];
            if (paramNames.length > 0) {
                Map<String, String> converterParams = getConverterParams(request);
                for (int i = 0; i < paramNames.length; i++) {
                    Class paramType = paramTypes[i];
                    if (paramType.isArray()) {
                        String[] paramValues = request.getParameterValues(paramNames[i]);
                        Converter converter = ConverterService.getInstance()
                                .getConverter(paramType.getComponentType());
                        if (converter != null) {
                            params[i] = converter.convert(paramValues, converterParams);
                        } else {
                            try {
                                if (paramValues != null) {
                                    Object arrayParam = Array
                                            .newInstance(paramType.getComponentType(), 
                                                    paramValues.length);
                                    for (int j = 0; j < paramValues.length; j++) {
                                        Array.set(arrayParam, j, 
                                                paramType.getComponentType().cast(paramValues[j]));
                                    }
                                    params[i] = arrayParam;
                                }
                            } catch (ClassCastException e) {
                                params[i] = null;
                            }
                        }
                    } else {
                        String paramValue = request.getParameter(paramNames[i]);
                        Converter converter = ConverterService.getInstance()
                                .getConverter(paramType);
                        if (converter != null) {
                            params[i] = converter.convert(paramValue, converterParams);
                        } else {
                            try {
                                params[i] = paramType.cast(paramValue);
                            } catch (ClassCastException e) {
                                params[i] = null;
                            }
                        }
                    }
                }
            }
        }
        return params;
    }
    
    private Map<String, String> getConverterParams(HttpServletRequest request) {
        Map<String, String> result = new HashMap<String, String>();
        Enumeration paramNames = request.getParameterNames();
        while (paramNames.hasMoreElements()) {
            String paramName = (String) paramNames.nextElement();
            if (paramName != null && 
                    paramName.startsWith("$")) {
                result.put(paramName, request.getParameter(paramName));
            }
        }
        return result;
    }
    
    protected Comparator<Method> getMethodComparator() {
        return new Comparator<Method>() {
            public int compare(Method m1, Method m2) {
                int m1ParamCount = m1.getParameterTypes().length;
                int m2ParamCount = m2.getParameterTypes().length;
                
                if (m1ParamCount > m2ParamCount) {
                    return -1;
                } else if (m1ParamCount < m2ParamCount) {
                    return 1;
                } else {
                    return 0;
                }
            }
        };
    }
    
    protected void print(String text) {
        if (text == null) return;
        
        if (request != null && 
                response != null) {
            try {
                PrintWriter writer = response.getWriter();
                writer.print(text);
            } catch (IOException ioe) {
                ioe.printStackTrace();
            }
        }
    }
    
    protected void forwardTo(String url) {
        if (request != null && 
                response != null) {
            try {
                request.getRequestDispatcher(url).forward(request, response);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    protected void redirectTo(String url) {
        if (request != null && 
                response != null) {
            try {
               response.sendRedirect(url);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
    
    protected void setAttribute(String name, Object value) {
        if (request != null && 
                response != null) {
            request.getSession().setAttribute(name, value);
        }
    }
    
    protected Object getAttribute(String name) {
        if (request != null && 
                response != null) {
            return request.getSession().getAttribute(name);
        } else {
            return null;
        }
    }
}
